# Copyright (c) 2008, RPI-ACM, Jesse Kempf <jessekempf@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# - Redistributions of source code must retain the above copyright notice,
#     this list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
# - Neither the names of RPI-ACM nor the names of its contributors
#     may be used to endorse or promote products derived from this software
#     without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
#  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
#  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
#  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
#  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
#  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
#  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
#  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
#  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""
PyNIX Privsep Command Framework

This module provides classes to support the CommandChannel both at a CPI and
internals level. The API is neither affected nor effected by the contents
herein.

CPI relevant stuffs are the Command, CommandPolicy and CommandContext
classes and the argtypes() decorator. The CommandPackage class provides a
simple way of collecting a suite of Commands into a single package.

Internals stuffs are the CommandTable class and the find_commands()
function.
"""

import inspect

class CommandPolicy(object):
	"""A CommandPolicy provides policy enforcement capability."""
	def __init__(self, owner):
		"""Create a CommandPolicy linked with its owner."""
		self.owner = owner
		owner.policy = self

	def policy_check(self, *args, **kwargs):
		"""ABSTRACT.

		Check to see if the provided conditions indicate a request
		should be honored."""
		raise NotImplementedError

	def set_rules(self, *rules):
		"""ABSTRACT.

		Set the ruleset to the one provided."""
		raise NotImplementedError

	def add_rule(self, rule):
		"""ABSTRACT.

		Add the provided rule to the ruleset."""
		raise NotImplementedError

	def add_rules(self, *rules):
		"""ABSTRACT.

		Add the provided rule to the ruleset."""
		raise NotImplementedError

class DenyAllPolicy(CommandPolicy):
	"""A stub policy that will always deny a request."""
	def policy_check(self, *args, **kwargs):
		"""Always return false."""
		return False

	def add_rules(self, *rules):
		"""No-op."""
		pass

	def set_rules(self, *rules):
		"""No-op"""
		pass

	def add_rule(self, rule):
		"""No-op."""
		pass

class CMDExceptionMeta(type):
	"""The metaclass for Exceptions to be sent across a CommandChannel."""
	def __new__(mcs, name, bases, dict_):
		klass = type.__new__(mcs, name, bases, dict_)
		klass.name = name
		klass.managesChannel = False

		return klass

class CMDException(Exception):
	"""The special base class for all Exceptions to be sent
	across a CommandChannel."""
	__metaclass__ = CMDExceptionMeta

	@classmethod
	def testable(cls):
		"""CMDExceptions do not implement test suites."""
		return False

	@classmethod
	def invoke(cls, *args, **kwargs):
		"""No-op."""
		pass

	@classmethod
	def handle(cls, *args, **kwargs):
		"""No-op."""
		pass

class Message(object):
	# pylint: disable-msg=R0201,R0903
	"""Base class for Commands."""
	def __init__(self, name):
		"""Create a Message."""
		self.name = name

	def testable(self):
		"""Message does not have any tests."""
		return False

	# pylint: enable-msg=R0201,R0903
class Command(Message):
	"""The base class for all Commands to be sent across a CommandChannel.

	A Command has several standard methods:
	name: the human-readable name for command
	handle: a handler function for the callee side of the channel.
	invoke: a handler function for the caller side of the channel.
	policy_check: a function to determine whether to allow
		a behavior, that wraps the policy provided by the command.
	testable: a boolean function that indicates whether the Command
		has an associated test suite.
	test_parent: callee-half of the test suite.
	test_child: caller-half of the test suite.
	"""

	@staticmethod
	def noop(*args, **kwargs):
		"""A no-op function."""
		pass

	def __init__(self, name, policy=DenyAllPolicy):
		"""Create a Command.

		If a policy is not provided, a DenyAllPolicy will be registered
		by default.
		"""
		Message.__init__(self, name)

		self.policy = None

		self.policy_register(policy)

	handle = noop
	invoke = noop
	test_parent = noop
	test_child = noop

	def policy_register(self, policy):
		"""Register the given CommandPolicy for this Command."""
		self.policy = policy(self)

	def policy_check(self, *args, **kwargs):
		"""Run the policy check for the registered CommandPolicy."""
		return self.policy.policy_check(*args, **kwargs)

	def testable(self):
		"""Determine whether the Command has an associated test suite."""
		return self.test_parent != Command.noop \
			or self.test_child != Command.noop

	def __str__(self):
		"""Return the string representation of the Command (that is, the name.)"""
		return self.name

class CommandContext(object):
	# pylint: disable-msg=R0903
	"""A context provided to a Command's invoke or handle methods by the
	CommandChannel.

	The context contains the following attributes:
		channel: a reference to the CommandChannel
		command: a reference to the Command being called.
	"""
	def __init__(self, chan, cmd):
		"""Create a context."""
		self.channel = chan
		self.command = cmd

	# pylint: enable-msg=R0903

class CommandPackage(object):
	# pylint: disable-msg=R0903
	"""A CommandPackage provides a convenient way of bundling a set of like
	Commands.

	The package intercepts attribute lookups to allow accessing the command
	based upon its name, so a Command with the name "BAR" registered in the
	CommandPackage called "foo" may be accessed as foo.BAR.
	"""

	def __init__(self, *args):
		"""Create a package containing the provided commands."""
		self.commands = dict([(arg.name, arg) for arg in args])

	def add_command(self, command):
		"""Add a command to the package."""
		self.commands[command.name] = command

	def __getattr__(self, attr):
		"""Resolve attributes based on command names."""
		try:
			return self.commands[attr]
		except KeyError:
			raise AttributeError, attr

	def __iter__(self):
		"""Get an iterator into the commands."""
		return iter(self.commands.values())

	def __str__(self):
		"""Get a string representation of the commands in the package."""
		return '<{' + ', '.join([str(x) for x in self]) + '}>'

	def __add__(self, rhs):
		"""Add the contents of another CommandPackage to this one."""
		for cmd in rhs:
			self.add_command(cmd)
		return self

	# pylint: enable-msg=R0903

def find_commands(container):
	"""Search for Commands in a dictionary, CommandPackage, or as attributes
	of any containerect. Return a list of Commands so found."""
	
	if isinstance(container, dict):
		attrs = container.values()
	elif isinstance(container, CommandPackage):
		attrs = list(container)
	else:
		attrs = [getattr(container, attr) for attr in dir(container)]

	cmds = [x() for x in attrs if inspect.isclass(x)
				and issubclass(x, Command)
				and x != Command]
	excs = [x for x in attrs if inspect.isclass(x)
				and issubclass(x, CMDException)
				and x != CMDException]
	msgs = [x for x in attrs if isinstance(x, Message)]

	pkg_cmds = [list(x) for x in attrs if isinstance(x, CommandPackage)]
	pkgc = []
	[pkgc.extend(p) for p in pkg_cmds]

	all_commands = cmds + excs + msgs + pkgc

	return all_commands

def obj(object_):
	"""Pass-through "type" to provide to argtypes()."""
	return object_

def argtypes(*typelist, **kwtypelist):
	"""A decorator to facilitate type conversions before a function is
	called.

	NB: Defaults in the function definition are not converted and must
	be of the correct type."""

	typelist = list(typelist)
	def decorate(func):
		"""Decorate."""
		(args, _, _, _) = inspect.getargspec(func)
		typelist2 = [kwtypelist[x] for x in args[len(typelist):]]
		typelist.extend(typelist2)
		def inner(*args, **kwargs):
			"""Inner."""
			args = [t(a) for t, a in zip(typelist, args)]
			kwargs = dict((key, kwtypelist[key](kwargs[key])) for key in kwargs)
			return func(*args, **kwargs)
		return inner
	return decorate
